Python + boto3でメール検証してるACMの証明書を列挙する
はじめに
私が参加しているプロジェクトはDNS検証に対応する以前からACMの証明書を利用していて、一部Eメール検証のものが残っています。証明書の更新を自動化するため、証明書の期限切れ通知を受け取ったタイミングで逐次DNS検証に切り替えていたのですが、都度対応するのが面倒になってきました。
そこで、切り替えが必要な証明書がどれくらい残っているか確認するため、AWS SDK for Python (Boto3)を使って、Eメール検証の証明書を列挙してみます。
前提条件
- macOS: 10.13.6
- CPython: 3.7.0
- Pipenv: 2018.7.1
- boto3: 1.9.14
環境構築
Pipenv を使って環境構築します。
$ pipenv install --python 3.7.0 boto3 colorama
colorama
は表示を見やすくするために使います。
やってみる
AWSアカウントごと・リージョンごとにEメール検証の証明書を列挙してみます。 ACMのドキュメント を確認すると、 ListCertificates と DescribeCertificate で 実現できそうです。
以下の手順で処理するプログラムを作成します。
- 任意のプロファイル・任意のリージョン用のACMクライアントを作成
- ACMの証明書一覧を取得する
- 各証明書の詳細情報を取得する
- ドメイン検証オプションの
ValidationMethod
がEMAIL
の場合、切り替えが必要と伝わるように表示する
def main(): profiles = ["default", "staging"] regions = ["ap-northeast-1", "us-east-1"] results = [r for r in enumerate_certificates(profiles, regions)] show_results(results)
enumerate_certificates
で手順1〜3の処理を、 show_results
で手順4の処理を行います。以降で enumerate_certificates
と show_results
を実装していきます。
完成版のソースは Gist に載せています。
各プロファイル・各リージョンの証明書を列挙する
各プロファイル・各リージョンのACMクライアントを作って、一覧取得・詳細取得のAPIを呼び出します。 Boto3のレスポンスはdict型なのでそのまま使っても良いのですが、(自分にとって)わかりやすいnamedtupleを使うようにしています。 enumerate_certificates
の戻り値も同様です。
import itertools from collections import namedtuple from boto3.session import Session EnumerateResult = namedtuple("EnumerateResult", ["profile", "region", "certificates"]) def enumerate_certificates(profiles, regions): for p, r in itertools.product(profiles, regions): # ACMのクライアントを生成 session = Session(profile_name=p, region_name=r) acm = session.client("acm") # 証明書の一覧を取得 summaries = list(list_certificates(acm)) # 証明書の詳細を取得 certificates = [describe_certificate(acm, s.arn) for s in summaries] # 結果を返却する yield EnumerateResult(profile=p, region=r, certificates=certificates)
証明書の一覧を取得する (ListCertificates)
任意のプロファイル・リージョン用のACMクライアントを使って、証明書の一覧を取得します。
from collections import namedtuple CertificateSummary = namedtuple("CertificateSummary", ["arn", "domain_name"]) def list_certificates(acm): def create_summary(s): arn = s["CertificateArn"] domain_name = s["DomainName"] return CertificateSummary(arn=arn, domain_name=domain_name) complete = False next_token = None while not complete: params = dict(NextToken=next_token) if next_token else {} res = acm.list_certificates(**params) next_token = res.get("NextToken", None) complete = next_token is None for x in res["CertificateSummaryList"]: yield create_summary(x)
証明書の詳細情報を取得する (DescribeCertificate)
詳細情報の取得も同様です。ACMのクライアントに加えて、対象の証明書のARNを使います。
from collections import namedtuple Certificate = namedtuple( "Certificate", ["arn", "domain_name", "status", "validation_options"] ) ValidationOption = namedtuple( "DomainValidationOptions", ["validation_status", "validation_method"] ) def describe_certificate(acm, cert_arn): def create_validation_option(o): status = o["ValidationStatus"] method = o["ValidationMethod"] return ValidationOption(validation_status=status, validation_method=method) res = acm.describe_certificate(CertificateArn=cert_arn) cert = res["Certificate"] options = cert.get("DomainValidationOptions", []) options = [create_validation_option(x) for x in options] return Certificate( arn=cert_arn, domain_name=cert["DomainName"], validation_options=options, status=cert["Status"], )
結果を表示する
最後に結果を表示します。証明書のステータスが ISSUED
のものを対象に、 ドメインの検証方法が DNS
の場合は ✓
を、 MAIL
の場合は ✗
を、それぞれ先頭に表示します。
import itertools import colorama from colorama import Fore def prefix_for(cert): def is_issued(): return cert.status == "ISSUED" def has_mail_validation(): return any([o.validation_method == "EMAIL" for o in cert.validation_options]) if is_issued(): prefix = Fore.RED + "✗" if has_mail_validation() else Fore.GREEN + "✓" prefix += Fore.RESET return prefix else: return "-" def show_results(results): colorama.init(autoreset=True) status_width = max([ len(c.status) for c in itertools.chain.from_iterable([x.certificates for x in results]) ]) for profile, profile_group in itertools.groupby(results, key=lambda x: x.profile): if not profile_group: continue print("-" * 40) print(f"[{profile}]") for result in profile_group: if not result.certificates: continue print(f"\n{result.region}") for c in result.certificates: prefix = prefix_for(c) status = c.status.ljust(status_width) methods = ",".join([o.validation_method for o in c.validation_options]) print(f"{prefix} {status} {c.domain_name} ({methods})")
動かすとこんな感じに表示します。
赤字で ✗
付きの証明書が対象のものです。
おわりに
今回はAWS SDK for Python (Boto3) を使って面倒な作業をスクリプト化してみました。画面から操作すると面倒なことでも簡単にプログラム化できることが多いので積極的にツール化していこうと思います。